昨日回顧
Day 20 建立任務型 eval,分開檢查 script、Skill 讀取路徑與最終答覆。今天把它變成每次變更都要通過的合併門檻。Day 12 已經把測試接進 CI;這次補上的不是另一個綠色勾勾,而是任務案例的身分、失敗分類、報告完整性與不可被略過的檢查。
以下是 ticket-source-guard 的設計範例,不代表已有一個實際 repository 跑過這套流程,也不宣稱任何真實購票來源有效。
先定義什麼叫通過
只看程序退出碼不夠。runner 可能只跑了一半案例,或者沒有產生報告,卻仍然回傳 0。合併門檻應同時核對:
1. 預定案例全部執行,case_id 無遺漏、重複或多出
2. 每筆案例的 schema、狀態與必要欄位都通過
3. 關鍵語義斷言通過,沒有不允許的官方或購買宣稱
4. 執行來源、測試集版本、固定日期與設定可追溯
5. 報告格式正確,沒有 timeout、error 或未完成的案例
關鍵案例失敗就停止合併。非關鍵措辭差異可以交人工審查,但不能把沒有審過的差異算成通過。缺報告是失敗,零案例也是失敗。
把 runner 與 gate 分開
runner 負責執行,gate 負責驗收。下面的 Python 只示範最小結構檢查;正式使用前,還要加入 JSON Schema、欄位型別與語義斷言驗證。
from collections import Counter
def gate(expected_ids, results):
expected = Counter(expected_ids)
actual = Counter(row["case_id"] for row in results)
if not expected or any(n != 1 for n in expected.values()):
raise ValueError("invalid case manifest")
if actual != expected:
raise ValueError("missing, duplicate, or unknown case")
if any(row["outcome"] != "pass" for row in results):
raise ValueError("eval failed or incomplete")
不要只寫 len(results) == 24。少跑一筆、多跑另一筆,總數仍然可能是 24。manifest 裡的 case_id 才是要核對的集合;每個 case_id 只允許出現一次。
這個函式也不會自行知道模型是否說錯。outcome 必須由 Day 20 的精確欄位比較與語義斷言產生,不能由模型自己宣告「我通過了」。
CI 的最小執行順序
checkout 固定 commit
→ 安裝鎖定版本的依賴
→ 驗證 registry 與 fixtures 的 schema
→ 跑 L1 單元測試
→ 跑 L2/L3 任務型 eval
→ gate 核對 manifest 與結果
→ 保存經過資料檢查的報告
下面是 GitHub Actions 的 job 步驟片段,不是可直接貼上就完整運作的 workflow。checkout、Python 環境與依賴安裝要在前面完成;這裡的兩支 script 也需要自己實作。
- name: Unit tests
run: python -m pytest -q
- name: Task eval
run: >-
python scripts/run_task_eval.py
--manifest tests/evals/manifest.json
--evaluation-date 2026-10-02
--output reports/task-eval.json
- name: Verify task eval
run: >-
python scripts/check_eval_report.py
--manifest tests/evals/manifest.json
--report reports/task-eval.json
固定日期是這組 fixture 的測試輸入,不是「現在時間」。runner 與 gate 都要遇到錯誤就用非零退出碼結束。不要用 continue-on-error: true 或 || true 把失敗吞掉。
對會執行 PR 程式碼的檢查,使用符合情境的 pull_request 事件,權限保持最小,例如 contents: read;不要把不可信 PR 程式碼接到有 secrets 或寫入權限的流程。正式採用的 actions 與依賴版本也要固定並接受審查。
綠色不代表合併門檻真的存在
workflow 跑成功,和 repository 設定要求它通過,是兩件事。將穩定且唯一的 check 名稱加入 branch protection 或 ruleset 的 required checks,並用一個故意失敗的 PR 驗證合併真的被擋下。
GitHub 文件提醒:workflow 若因 path、branch 或 commit-message 過濾而整個略過,required check 可能停在 Pending;job 因條件略過,卻可能被視為成功。依賴失敗 job 的 gate 也不能默默略過。若採用 needs,要讓最後的 gate 即使上游失敗仍執行,再明確檢查上游結果,而不是只加 always() 就算完成。
因此小專案可以先讓 required workflow 對每個 PR 都執行,不急著做 path filter。真的要省成本時,再明確定義哪些變更需要完整 eval,並驗證所有分支都有正確的 gate 結果。若使用 merge queue,也要照文件補上 merge_group 觸發。
失敗報告要能讓人直接重現
以下是示意資料,不是真正的測試結果:
{
"case_id": "stale-registered-host",
"layer": "L3",
"outcome": "fail",
"expected_status": "UNCONFIRMED",
"actual_status": "OFFICIAL",
"failed_assertion": "stale_evidence_requires_revalidation",
"fixture_version": "eval-set-3",
"evaluation_date": "2026-10-02",
"source_commit": "<candidate commit SHA>",
"next_action": "fix freshness classification and rerun all cases"
}
完整報告另外記錄 manifest、registry 與設定的 digest,以及 runner、grader、模型版本和執行時間。用這些欄位找回同一組輸入,不要只附一句「eval 失敗」。不要為了讓 CI 變綠,直接把 expected 改成模型剛剛答的結果;預期行為的變更必須連回 Day 19 的變更請求。
保留報告,但不要保留秘密
artifact 可以保存最小輸入、正規化輸出與失敗斷言,不應包含 API key、cookie、私人對話或整份未檢查的工具紀錄。先用 fixtures 重現;需要真實案例時,先去識別化並確認可保存的範圍。上傳步驟即使在 eval 失敗時執行,也不應把原始敏感資料一起上傳。
模型型 eval 若有波動,先固定設定、保存每次結果並制定重跑規則。不要無限重跑到第一次成功,再把先前失敗當作沒發生。
今天完成的驗收標準
[ ] manifest 非空且 case_id 唯一,結果逐筆核對
[ ] 缺報告、零案例、timeout 或 error 都不放行
[ ] 狀態與關鍵語義不能被總通過率掩蓋
[ ] required check 在最新變更上實際擋住失敗 PR
[ ] 略過條件、上游失敗與 merge queue 都有驗證
[ ] 報告可重現,artifact 不含秘密或私人原文
參考資料
GitHub Actions workflow syntax:
https://docs.github.com/actions/reference/workflow-syntax-for-github-actions
Troubleshooting required status checks:
https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks
明天預告
Day 22 把失敗案例整理成可維護的測試資料集:案例來源、去識別化、版本與淘汰規則,避免 eval 越長越難懂。